﻿using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Net;
using System.Net.Sockets;
using System.Threading;

namespace SimpleTFTP
{
    internal class ServerEventArgs : EventArgs
    {
        private string data;

        public ServerEventArgs(string _data)
        {
            this.data = _data;
        }

        public string Data { get { return this.data; } }
    }

    internal class Server : IDisposable
    {
        #region Events

        public delegate void ErrorEventHandler(object sender, ServerEventArgs e);

        public event ErrorEventHandler OnError;

        #endregion Events

        #region Server Options

        private bool serverCanReadFiles = true;
        private bool serverCanWriteFiles = true;

        private int serverDefaultBlockSize = 512;

        public static string serverBasePath = @"D:\TFTPROOT";

        // extended options

        private bool serverNegotiateOptions = true;

        private int blksize = 0;
        private int tsize = 0;
        private int timeout = 0;

        #endregion Server Options

        #region Server Config

        public const int DEFAULT_PORT = 69;
        private int port;
        private Socket socket;
        private Dictionary<EndPoint, Request> clients;

        #endregion Server Config

        public Server(int port)
        {
            this.port = port;
            this.socket = new Socket(AddressFamily.InterNetwork, SocketType.Dgram, ProtocolType.Udp);
            this.clients = new Dictionary<EndPoint, Request>();
        }

        public Server()
            : this(DEFAULT_PORT)
        { }

        public void Start()
        {
            new Thread(StartInternal).Start();
        }

        private void StartInternal()
        {
            Console.WriteLine("Server started. Listening");

            EndPoint endpoint = new IPEndPoint(IPAddress.Any, port);
            socket.Bind(endpoint);

            while (true)
            {
                if (socket.Poll(-1, SelectMode.SelectRead))
                {
                    byte[] buffer = new byte[socket.Available];
                    int bytesRead = socket.ReceiveFrom(buffer, ref endpoint);

                    BasePacket packet = BasePacket.CreatePacket(buffer);

                    // let's check for an existing session
                    //

                    if (!clients.ContainsKey(endpoint))
                    {
                        // if entry non-existant - great. continue
                        // let's create new request
                        //

                        Request request = new Request();
                        request.Client = endpoint;
                        request.OpCode = packet.OpCode;

                        // add it to our 'database'
                        //

                        clients.Add(endpoint, request);

                        switch (packet.OpCode)
                        {
                            case OpCode.RRQ:

                                // check if able to read files
                                //

                                if (serverCanReadFiles)
                                {
                                    // Do serious stuff here
                                    //

                                    ProcessReadPacket(request, packet);
                                }
                                else
                                {
                                    //error
                                }
                                break;

                            case OpCode.WRQ:

                                // can we write files to server?
                                //

                                if (serverCanWriteFiles)
                                {
                                    // yup, let's get it going
                                    //

                                    ProcessWritePacket(request, packet);
                                }
                                else
                                {
                                    //error
                                }
                                break;

                            default:

                                // send error to client
                                //

                                SendError(request, ErrorCode.ILLEGAL_OPERATION, "Illegal operation.");
                                Error(new ServerEventArgs("Illegal operation."));
                                break;
                        }
                    }
                    else
                    {
                        Request request = clients[endpoint];

                        switch (packet.OpCode)
                        {
                            case OpCode.RRQ:
                            case OpCode.WRQ:

                                // send error
                                //

                                SendError(request, ErrorCode.ILLEGAL_OPERATION, "Out of place RRQ/WRQ request.");
                                Error(new ServerEventArgs("Out of place RRQ/WRQ request."));
                                break;

                            case OpCode.ACK:

                                // let's process ACK
                                //

                                ProcessACK(request, packet);
                                break;

                            case OpCode.DATA:

                                // process incoming DATA packet
                                // TODO

                                ProcessData(request, packet);

                                break;

                            default:

                                // send error
                                //

                                SendError(request, ErrorCode.ILLEGAL_OPERATION, "Unknown opcode. Try again.");
                                Error(new ServerEventArgs("Unknown opcode."));
                                break;
                        }
                    }
                }
                else
                {
                    // let's just waste some time here :)
                    //

                    Thread.Sleep(10);
                }
            }
        }

        private void ProcessData(Request request, BasePacket packet)
        {
            //throw new NotImplementedException();

            DataPacket dPacket = packet as DataPacket;

            if (dPacket.BlockNumber != request.LastBlockNoSent + 1)
            {
                //error

                SendError(request, ErrorCode.NOT_DEFINED, "Block number not in order.");
                Error(new ServerEventArgs("Block number not in order."));
                return;
            }
            else
            {
                if (request.LastBlockNoSent == 0)
                { 
                    request.Data = new List<byte>();
                }
                
                request.Data.AddRange(dPacket.Data);
                request.LastBlockNoReceived = dPacket.BlockNumber;
                request.LastBlockNoSent++;

                AckPacket ackPacket = new AckPacket(request.LastBlockNoSent);
                SendDataToClient(request, ackPacket.ToByteArray());

                if (dPacket.Data.Length < 512)
                {
                    request.RequestComplete = true;

                    using (BinaryWriter br = new BinaryWriter(File.Create(request.FileName)))
                    {
                        br.Write(request.Data.ToArray());
                    }

                    clients.Remove(request.Client);
                }
            }
        }

        private void ProcessWritePacket(Request request, BasePacket packet)
        {
            ReadWritePacket wPacket = packet as ReadWritePacket;
            request.FileName = Path.Combine(serverBasePath, Path.GetFileName(wPacket.FileName));


            if (!File.Exists(request.FileName))
            {
                // all good, send ACK
                //

                request.LastBlockNoSent = 0;
                AckPacket ackPacket = new AckPacket(0);
                SendDataToClient(request, ackPacket.ToByteArray());
            }
            else
            {
                // send error, file exists
                //

                SendError(request, ErrorCode.FILE_EXISTS, String.Format("File \"{0}\" already exists.", request.FileName));
                Error(new ServerEventArgs(String.Format("File \"{0}\" already exists.", request.FileName)));
            }
        }

        private void ProcessACK(Request request, BasePacket packet)
        {
            AckPacket ackPacket = packet as AckPacket;

            Console.WriteLine("Got ACK packet from: {0} BLOCKNO: {1}", request.Client, ackPacket.BlockNo);

            if (ackPacket.BlockNo != request.LastBlockNoReceived + 1)
            {
                // Something went wrong. block number not in order. send error
                //

                SendError(request, ErrorCode.NOT_DEFINED, "Block number not in order.");
                Error(new ServerEventArgs("Block number not in order."));
                return;
            }

            if (!request.RequestComplete)
            {
                request.LastBlockNoReceived = ackPacket.BlockNo;

                // send next block of data
                //

                SendNextDataPacket(request);
            }
            else
            {
                // transfer complete. remove request from list.
                //

                clients.Remove(request.Client);
            }
        }

        private void ProcessReadPacket(Request request, BasePacket packet)
        {
            ReadWritePacket rwPacket = packet as ReadWritePacket;
            request.FileName = Path.Combine(serverBasePath, Path.GetFileName(rwPacket.FileName));

            // have we got such a file?
            //

            if (File.Exists(request.FileName))
            {
                // we have!
                //

                request.Data = File.ReadAllBytes(request.FileName).ToList();
                if (request.Data != null)
                {
                    request.LastBlockNoReceived = 0;
                    Console.WriteLine("Sending file {0} to {1}", request.FileName, request.Client);

                    SendNextDataPacket(request);
                }
                else
                {
                    // error reading file or smth. report!
                    //

                    SendError(request, ErrorCode.ACCESS_VIOLATION, "Error reading file.");
                    Error(new ServerEventArgs("Error reading file."));
                }
            }
            else
            {
                // file does not exist. report error!
                //

                SendError(request, ErrorCode.FILE_EXISTS, String.Format("File \"{0}\" not found.", request.FileName));
                Error(new ServerEventArgs(String.Format("File \"{0}\" not found.", request.FileName)));
            }
        }

        private void SendError(Request request, ErrorCode errCode, string errMessage)
        {
            ErrorPacket errPacket = new ErrorPacket((short)errCode, errMessage);
            SendDataToClient(request, errPacket.ToByteArray());
        }

        private void SendDataToClient(Request request, byte[] data)
        {
            socket.SendTo(data, request.Client);
        }

        private void SendNextDataPacket(Request request)
        {
            byte[] sendData = request.GetNextDataBlock();

            DataPacket packet = new DataPacket(sendData, (short)(request.LastBlockNoReceived + 1));
            SendDataToClient(request, packet.ToByteArray());

            if (sendData.Length == 0 || sendData.Length < serverDefaultBlockSize)
            {
                Console.WriteLine("Last data packet for {0}", request.Client);
                request.RequestComplete = true;
            }
        }

        public void Dispose()
        {
            socket.Dispose();
        }

        protected virtual void Error(ServerEventArgs e)
        {
            if (OnError != null)
            {
                OnError(this, e);
            }
            else
            {
                Console.WriteLine("Error: ");
            }
        }
    }
}